<?php
/**
 * @link https://www.len168.com
 * @copyright Copyright (c) 2020/9/15 len168.com
 * @author toshcn <toshcn@foxmail.com>
 */

namespace common\components\payment;

use Yii;
use Alipay\EasySDK\Kernel\Factory;
use Alipay\EasySDK\Kernel\Config;

/**
 * 支付宝
 * @package common\components\payment
 */
class Alipay extends DefaultPayment implements PaymentInterface
{
    private $options = null;
    /**
     * 初始化
     */
    public function init()
    {
        parent::init();
    }

    /**
     * 配置组件
     * @param array $config 配置数组
     */
    public function setConfig($config)
    {
        $options = new Config();
        $options->protocol = 'https';
        $options->gatewayHost = 'openapi.alipay.com';
        $options->signType = 'RSA2';

        if (is_array($config)) {
            // 支付宝appId
            $options->appId = isset($config['appId']) ? trim($config['appId']) : '';

            // 为避免私钥随源码泄露，推荐从文件中读取私钥字符串而不是写入源码中
            //$options->merchantPrivateKey = '<-- 请填写您的应用私钥，例如：MIIEvQIBADANB ... ... -->';
            $options->merchantPrivateKey = isset($config['merchantPrivateKey']) ? trim($config['merchantPrivateKey']) : '';

            //$options->alipayCertPath = '<-- 请填写您的支付宝公钥证书文件路径，例如：/foo/alipayCertPublicKey_RSA2.crt -->';
            //$options->alipayRootCertPath = '<-- 请填写您的支付宝根证书文件路径，例如：/foo/alipayRootCert.crt" -->';
            //$options->merchantCertPath = '<-- 请填写您的应用公钥证书文件路径，例如：/foo/appCertPublicKey_2019051064521003.crt -->';
            // 证书文件请放在当前cert目录下
            $options->alipayCertPath = __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'alipay' . DIRECTORY_SEPARATOR . 'alipayCertPublicKey_' . $options->signType . '.crt';
            $options->alipayRootCertPath = __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'alipay' . DIRECTORY_SEPARATOR . 'alipayRootCert.crt';
            $options->merchantCertPath = __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'alipay' . DIRECTORY_SEPARATOR . 'appCertPublicKey_' . $options->appId . '.crt';


            //注：如果采用非证书模式，则无需赋值上面的三个证书路径，改为赋值如下的支付宝公钥字符串即可
            //$options->alipayPublicKey = '<-- 请填写您的支付宝公钥，例如：MIIBIjANBg... -->';
            $options->alipayPublicKey = isset($config['alipayPublicKey']) ? trim($config['alipayPublicKey']) : '';

            //可设置异步通知接收服务地址（可选）
            //$options->notifyUrl = "<-- 请填写您的支付类接口异步通知接收服务地址，例如：https://www.test.com/callback -->";
            $options->notifyUrl = isset($config['notifyUrl']) ? trim($config['notifyUrl']) : '';

            //可设置AES密钥，调用AES加解密相关接口时需要（可选）
            //$options->encryptKey = "<-- 请填写您的AES密钥，例如：aa4BtZ4tspm2wnXLb1ThQA== -->";
            $options->encryptKey = isset($config['encryptKey']) ? trim($config['encryptKey']) : '';

        }
        $this->options = $options;
        // 设置参数
        Factory::setOptions($this->options);
    }

    /**
     * 创建支付订单
     * @param array $order 订单数组
     * string $order['title'] 订单标题
     * string $order['no'] 商户订单号，64个字符以内，可包含字母、数字、下划线，需保证在商户端不重复
     * string $order['money'] 订单总金额，单位为元
     * string $order['buyerId'] 买家的支付宝唯一用户号（2088开头的16位纯数字）
     * @param string $form 订单用处 app, h5等
     * @return array
     */
    public function createPayOrder($order = [], $form = 'app')
    {
        try {
            //2. 发起API调用（以支付能力下的统一收单交易创建接口为例）
            $result = Factory::payment()->common()->create($order['title'], $order['no'], $order['money'], $order['buyerId']);

            //3. 处理响应或异常
            if (!empty($result->code) && $result->code == 10000) {
                return ['status' => 1, 'data' => $this->formatPayOrderInfo($order, $form), 'trade_no' => $result->tradeNo];
            }

            return ['status' => 0, 'data' => [], 'message' => "接口调用失败：". $result->msg."，".$result->subMsg];
        } catch (\Exception $e) {
            return ['status' => 0, 'data' => [], 'message' => $e->getMessage()];
        }
    }

    /**
     * 支付宝转账接口
     * @param array $order
     * string $order['title'] 订单标题
     * string $order['no'] 商户订单号，64个字符以内，可包含字母、数字、下划线，需保证在商户端不重复
     * string $order['money'] 订单总金额，单位为元
     * string $order['account'] 用户支付宝唯一用户号（2088开头的16位纯数字）
     * string $order['realname'] 转账对方的真实姓名
     * string $order['remark'] 转账备注
     * @return array
     * @throws \Exception
     */
    public function transMoney($order = [])
    {
        try {
            //2. 发起API调用
            $result = Factory::util()->generic()->execute('alipay.fund.trans.uni.transfer', [], [
                'out_biz_no' => $order['no'],
                'trans_amount' => $order['money'],
                'product_code' => 'TRANS_ACCOUNT_NO_PWD',
                'order_title' => $order['title'],
                'biz_scene' => 'DIRECT_TRANSFER',
                'payee_info' => [
                    'identity' => $order['account'],
                    'identity_type' => 'ALIPAY_USER_ID',
                    'name' => $order['realname']
                ],
                'remark' => $order['remark']
            ]);
            //3. 处理响应或异常
            if (!empty($result->code) && $result->code == 10000) {
                $body = json_decode($result->httpBody, true);
                return ['status' => 1, 'data' => $body['alipay_fund_trans_uni_transfer_response']];
            }
            return ['status' => 0, 'data' => [], 'message' => "接口调用失败：". $result->msg."，".$result->subMsg];
        } catch (\Exception $e) {
            return ['status' => 0, 'data' => [], 'message' => $e->getMessage()];
        }
    }

    /**
     * 查询接口
     * @param string $tradeNo 订单号
     * @return array
     */
    public function queryPayOrder($tradeNo)
    {
        try {
            //2. 发起API调用
            $result = Factory::payment()->common()->query($tradeNo);

            //3. 处理响应或异常
            if (!empty($result->code) && $result->code == 10000) {
                return ['status' => 1, 'data' => [
                    'tradeStatus' => $result->tradeStatus,
                    'tradeNo' => $result->tradeNo,
                    'totalAmount' => $result->totalAmount,
                    'buyerUserId' => $result->buyerUserId
                ]];
            }

            return ['status' => 0, 'data' => [], 'message' => "接口调用失败：". $result->msg."，".$result->subMsg];
        } catch (\Exception $e) {
            return ['status' => 0, 'data' => [], 'message' => $e->getMessage()];
        }
    }

    /**
     * 整理订单数据
     * @param array $order
     * @param string $form 生成的订单用处：app，h5等
     * @return string
     */
    private function formatPayOrderInfo($order, $form = 'app')
    {
        // 按字母排序 可以用ksort($arr)排序，也可以手动排好序
        $arr['alipay_root_cert_sn'] = $this->getRootCertSN($this->options->alipayRootCertPath);
        $arr['app_cert_sn'] = $this->getRootCertSN($this->options->merchantCertPath);
        $arr['app_id'] = $this->options->appId;
        $arr['biz_content'] = json_encode([
            'body' => $order['title'],
            'buyer_id' => $order['buyerId'],
            'subject' => $order['title'],
            'out_trade_no' => $order['no'],
            'timeout_express' => '30m',
            'total_amount' => $order['money'],
            'product_code' => 'QUICK_MSECURITY_PAY', //销售产品码。在 App 支付场景下必填，且固定为： QUICK_MSECURITY_PAY。
        ], JSON_UNESCAPED_UNICODE);
        $arr['charset'] = 'UTF-8';
        $arr['format'] = 'json';
        $arr['notify_url'] = $this->options->notifyUrl;
        $arr['method'] = 'alipay.trade.app.pay';
        $arr['sign_type'] = $this->options->signType;
        $arr['timestamp'] = date('Y-m-d H:i:s');
        $arr['version'] = '1.0';

        // 签名并编码
        $sign = $this->signAndEncode($arr);
        unset($arr);
        return $sign;
    }

    /**
     * 签名并编码 用于App支付
     * @param $data
     * @param bool $isSort 是否需要排序
     * @return string
     */
    public function signAndEncode($data, $isSort = true)
    {
        // 排序
        if ($isSort === true) ksort($data);
        // 拼接字符串
        $buff = '';
        // 待签名字符串
        $signStr = '';
        $i = 0;
        foreach ($data as $k => $v) {
            if ($i == 0) {
                $signStr .=  $k . '=' . $v;
                $buff .= $k . '=' . urlencode($v);
            } else {
                $signStr .=  '&' . $k . '=' . $v;
                $buff .= '&' . $k . '=' . urlencode($v);
            }
            $i++;
        }
        unset($k, $v);

        $sign = $this->sign($signStr, $this->options->signType);

        // 返回签名后的字符串
        return $buff . '&sign=' . urlencode($sign);
    }

    /**
     * 签名
     * @param string $signStr 待签名字符串
     * @param string $signType 加密方式
     * @return string
     */
    public function sign($signStr, $signType = 'RSA2')
    {
        /** @value $this->options->merchantPrivateKey RSA私钥 */
        $res = "-----BEGIN RSA PRIVATE KEY-----\n" .
            wordwrap($this->options->merchantPrivateKey, 64, "\n", true) .
            "\n-----END RSA PRIVATE KEY-----";

        if ($signType == 'RSA2') {
            openssl_sign($signStr, $sign, $res, OPENSSL_ALGO_SHA256);
        } else {
            openssl_sign($signStr, $sign, $res);
        }
        $sign = base64_encode($sign);
        return $sign;
    }


    /**
     * 创建订单号
     * @param int $uid 用户id
     * @return string
     */
    public function createTradeNo($uid)
    {
        return 'a' . date('YmdHis') . $uid . mt_rand(1000, 99999);
    }

    /**
     * 异步通知验签
     * @param array $params 异步通知中收到的待验签的所有参数
     * @return bool
     * @throws \Exception
     */
    public function verifyNotify($params = [])
    {
        // 设置参数
        Factory::setOptions($this->options);
        return Factory::payment()->common()->verifyNotify($params);
    }

    /**
     * 从证书中提取序列号 [@see https://www.freesion.com/article/9400246878/]
     * @param string $merchantCertPath cert证书绝对路径
     * @return string
     */
    public function getCertSN($merchantCertPath)
    {
        $cert = file_get_contents($merchantCertPath);
        $ssl = openssl_x509_parse($cert);
        $SN = md5($this->array2string(array_reverse($ssl['issuer'])) . $ssl['serialNumber']);
        return $SN;
    }

    /**
     * 提取根证书序列号
     * @param string $alipayRootCertPath 根证书绝对路径
     * @return string|null
     */
    public function getRootCertSN($alipayRootCertPath)
    {
        $cert = file_get_contents($alipayRootCertPath);
        $array = explode('-----END CERTIFICATE-----', $cert);
        $SN = null;
        for ($i = 0; $i < count($array) - 1; $i++) {
            $ssl[$i] = openssl_x509_parse($array[$i] . '-----END CERTIFICATE-----');
            if (strpos($ssl[$i]['serialNumber'], '0x') === 0) {
                $ssl[$i]['serialNumber'] = $this->hex2dec($ssl[$i]['serialNumber']);
            }
            if ($ssl[$i]['signatureTypeLN'] == 'sha1WithRSAEncryption' || $ssl[$i]['signatureTypeLN'] == 'sha256WithRSAEncryption') {
                if ($SN == null) {
                    $SN = md5($this->array2string(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']);
                } else {

                    $SN = $SN . "_" . md5($this->array2string(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']);
                }
            }
        }
        return $SN;
    }
    /**
     * 0x转高精度数字
     * @param $hex
     * @return int|string
     */
    protected function hex2dec($hex)
    {
        $dec = 0;
        $len = strlen($hex);
        for ($i = 1; $i <= $len; $i++) {
            $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
        }
        return $dec;
    }

    /**
     * 数组转为字符串
     * @param $array
     * @return string
     */
    protected function array2string($array)
    {
        $string = [];
        if ($array && is_array($array)) {
            foreach ($array as $key => $value) {
                $string[] = $key . '=' . $value;
            }
        }
        return implode(',', $string);
    }

}
